﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using CitizenDiary.Sync.Shared;
using CitizenDiary.Sync.Contract;
using CitizenDiary.Sync.Service;
using Newtonsoft.Json;

namespace CitizenDiary.Sync.DeviceEmu
{
    class Program
    {
        private static string _database = ConfigurationManager.AppSettings["database"];
        private static Guid _deviceId = Guid.Parse(ConfigurationManager.AppSettings["deviceId"]);

        static void Main(string[] args)
        {
            ISyncService proxy = new SyncService();
            var info = proxy.Ping(new PingRequest() { DeviceID = _deviceId });

            Console.WriteLine("[INF] Citizen Diary Sync Device Emulator");
            Console.WriteLine("[INF] Device ID = {0}.", _deviceId);
            Console.WriteLine("[INF] Device Name = {0}.", info.Device.Value);
            Console.WriteLine("[INF] Connection String = {0}.", Helper.GetConnectionString(_database));
            Console.WriteLine();

            ShowTips();
            var command = Prompt();
            while (string.Compare(command, "q", true) != 0)
            {
                try
                {
                    switch (command)
                    {
                        case "a":
                        case "A":
                            AppendRecord();
                            break;
                        case "e":
                        case "E":
                            EditRecord();
                            break;
                        case "d":
                        case "D":
                            DeleteRecord();
                            break;
                        case "l":
                        case "L":
                            ListLocalRecords();
                            break;
                        case "s":
                        case "S":
                            Sync();
                            break;
                        default:
                            ShowTips();
                            break;
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("[ERR] {0}", ex.ToString());
                }
                finally
                {
                    command = Prompt();
                }
            }
        }

        private static void AppendRecord()
        {
            Console.WriteLine("[CMD] Please input the content of this diary item.");
            var content = Prompt();

            if (!string.IsNullOrWhiteSpace(content))
            {
                var version = PopUpdateVersion();
                using (var conn = Helper.CreateConnection(_database))
                {
                    using (var cmd = conn.CreateCommand())
                    {
                        cmd.CommandText = string.Format("INSERT INTO DiaryItem (Content, UpdatedVersion, UpdatedDeviceID, BasedVersion, BasedDeviceID) VALUES ('{0}', {1}, '{2}', {3}, '{4}')",
                            content, version, _deviceId, version, _deviceId);
                        cmd.ExecuteNonQuery();
                    }
                }
            }
        }

        private static void EditRecord()
        {
            Console.WriteLine("[CMD] Please input the diary item id.");
            var id = Guid.Parse(Prompt());
            Console.WriteLine("[CMD] Please input the new content of this diary item.");
            var content = Prompt();

            if (!string.IsNullOrWhiteSpace(content))
            {
                var version = PopUpdateVersion();
                using (var conn = Helper.CreateConnection(_database))
                {
                    using (var cmd = conn.CreateCommand())
                    {
                        cmd.CommandText = string.Format("UPDATE DiaryItem SET Content = '{0}', UpdatedVersion = {1}, UpdatedDeviceID = '{2}', UpdatedOn = '{4}' WHERE ID = '{3}'",
                            content, version, _deviceId, id, ToGlobalDateTimeString(DateTime.Now));
                        var rows = cmd.ExecuteNonQuery();
                        Console.WriteLine("[INF] {0} rows affected.", rows);
                    }
                }
            }
        }

        private static void DeleteRecord()
        {
            Console.WriteLine("[CMD] Please input the diary item id you want to delete.");
            var id = Guid.Parse(Prompt());

            var version = PopUpdateVersion();
            using (var conn = Helper.CreateConnection(_database))
            {
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = string.Format("UPDATE DiaryItem SET IsDeleted = 1, UpdatedVersion = {0}, UpdatedDeviceID = '{1}', UpdatedOn = '{3}' WHERE ID = '{2}'",
                        version, _deviceId, id, ToGlobalDateTimeString(DateTime.Now));
                    var rows = cmd.ExecuteNonQuery();
                    Console.WriteLine("[INF] {0} rows affected.", rows);
                }
            }
        }

        private static void ListLocalRecords()
        {
            using (var conn = Helper.CreateConnection(_database))
            {
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = "SELECT * FROM DiaryItem WHERE IsDeleted = 0";
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            Console.Write("[DAT] ");
                            for (var i = 0; i < reader.FieldCount; i++)
                            {
                                Console.Write("{0}\t", reader[i]);
                            }
                            Console.WriteLine();
                        }
                    }
                }
            }
        }

        private static void Sync()
        {
            // initialize the service
            ISyncService proxy = new SyncService();

            // ping the service to retrieve the current device information
            var pingResponse = proxy.Ping(new PingRequest() { DeviceID = _deviceId });
            Console.WriteLine("[SYN] Member     : {0}.", pingResponse.Member.Value);
            Console.WriteLine("[SYN] Application: {0}.", pingResponse.Application.Value);
            Console.WriteLine("[SYN] Device     : {0}.", pingResponse.Device.Value);
            Console.WriteLine("[SYN] =====");

            // retrieve the last sync date time
            var lastSyncOn = GetLastSyncOn();
            Console.WriteLine("[SYN] Last Sync  : {0}.", lastSyncOn);
            Console.WriteLine("[SYN] =====");

            // retrieve the current date time of sync
            // should retrieve the date time at the beginning of sync to avoid the time gap during the sync
            var thisSyncOn = DateTime.Now;

            // handshake
            var handshakeRequest = new HandshakeRequest();
            var items = new Dictionary<Guid, HandshakeRequest.DeviceDiaryItem>();
            // retrieve all data from device which updated after the last sync time
            using (var conn = Helper.CreateConnection(_database))
            {
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = string.Format("SELECT ID, UpdatedVersion, UpdatedDeviceID, BasedVersion, BasedDeviceID FROM DiaryItem WHERE UpdatedOn > '{0}'", lastSyncOn);
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            items.Add((Guid)reader["ID"], new HandshakeRequest.DeviceDiaryItem()
                                {
                                    UpdatedDeviceID = (Guid)reader["UpdatedDeviceID"],
                                    UpdatedVersion = (long)reader["UpdatedVersion"],
                                    BasedDeviceID = (Guid)reader["BasedDeviceID"],
                                    BasedVersion = (long)reader["BasedVersion"]
                                });
                        }
                    }
                }
            }
            handshakeRequest.DeviceItems = items;
            handshakeRequest.LastSyncOn = lastSyncOn;
            handshakeRequest.DeviceID = _deviceId;

            // invoke the handshake service
            var handshakeResponse = proxy.Handshake(handshakeRequest);
            // output the service invoke payload (based on the item count)
            Console.WriteLine("[DBG] Handshake Request Payload  : {0}", handshakeRequest.DeviceItems.Count);
            Console.WriteLine("[DBG] Handshake Response Payload : {0}", handshakeResponse.SynchronizationItems.Count);
            // output the service invoke payload (based on json format)
            Console.WriteLine("[DBG] Request Payload  : {0}", JsonConvert.SerializeObject(handshakeRequest).Length);
            Console.WriteLine("[DBG] Response Payload : {0}", JsonConvert.SerializeObject(handshakeResponse).Length);

            // synchronize
            var synchronizeRequest = new SynchronizeRequest();
            //var uploads = new SynchronizationCollection();
            using (var conn = Helper.CreateConnection(_database))
            {
                using (var cmd = conn.CreateCommand())
                {
                    foreach (var item in handshakeResponse.SynchronizationItems)
                    {
                        // solve the conflict if any and prepare the upload items
                        if (item.Value.Type == SynchronizationType.Conflict)
                        {
                            // show conflict message and let the user choose to use local data or server data
                            cmd.CommandText = string.Format("SELECT Content FROM DiaryItem WHERE ID = '{0}'", item.Key);
                            var localContent = (string)cmd.ExecuteScalar();
                            Console.WriteLine("[ASK] A conflict detected in the sync item (ID = {0}).", item.Key);
                            Console.WriteLine("[ASK] Local content is '{0}';", localContent);
                            Console.WriteLine("[ASK] Server content is '{0}'; uploaded by device '{1}'.", item.Value.Content, item.Value.UpdatedDeviceID);
                            Console.WriteLine("[ASK] Press solve it by:");
                            Console.WriteLine("[ASK] l - Use the local copy");
                            Console.WriteLine("[ASK] s - Use the service copy");
                            var slove = Prompt();
                            if (string.Compare(slove, "l", true) == 0)
                            {
                                // use the local version
                                item.Value.Type = SynchronizationType.Post;
                            }
                            else
                            {
                                // use the service version
                                item.Value.Type = SynchronizationType.Get;
                            }
                        }
                        // prepare the data the need to be upload to the server
                        if (item.Value.Type == SynchronizationType.Post || item.Value.Type == SynchronizationType.Put)
                        {
                            cmd.CommandText = string.Format("SELECT * FROM DiaryItem WHERE ID = '{0}'", item.Key);
                            using (var reader = cmd.ExecuteReader())
                            {
                                if (reader.Read())
                                {
                                    item.Value.Content = (string)reader["Content"];
                                    item.Value.UpdatedVersion = (long)reader["UpdatedVersion"];
                                    item.Value.UpdatedDeviceID = (Guid)reader["UpdatedDeviceID"];
                                    item.Value.IsDeleted = (bool)reader["IsDeleted"];
                                    item.Value.UpdatedOn = (DateTime)reader["UpdatedOn"];
                                }
                            }
                        }
                    }
                    // copy the sync items with the device data
                    // except those ignore since they don't need to be synced
                    handshakeResponse.SynchronizationItems
                        .Where(s => s.Value.Type != SynchronizationType.Ignore)
                        .All(s =>
                        {
                            synchronizeRequest.SynchronizationItems.Add(s.Key, new SynchronizationItem(s.Value));
                            return true;
                        });
                }
            }
            synchronizeRequest.DeviceID = _deviceId;
            synchronizeRequest.Token = handshakeResponse.Token;
            synchronizeRequest.SynchronizationKey = handshakeResponse.SynchronizationKey;

            // invoke the synchronize
            var synchronizeResponse = proxy.Synchronize(synchronizeRequest);
            // output the service invoke payload (based on the item count)
            Console.WriteLine("[DBG] Synchronize Request Payload  : {0}", synchronizeRequest.SynchronizationItems.Count);
            Console.WriteLine("[DBG] Synchronize Response Payload : {0}", synchronizeResponse.SynchronizationItems.Count);
            // output the service invoke payload (based on json format)
            Console.WriteLine("[DBG] Request Payload  : {0}", JsonConvert.SerializeObject(synchronizeRequest).Length);
            Console.WriteLine("[DBG] Response Payload : {0}", JsonConvert.SerializeObject(synchronizeResponse).Length);

            using (var conn = Helper.CreateConnection(_database))
            {
                var tran = conn.BeginTransaction();
                using (var cmd = conn.CreateCommand())
                {
                    cmd.Transaction = tran;
                    try
                    {
                        // merge the items from service side
                        foreach (var download in synchronizeResponse.SynchronizationItems)
                        {
                            if (download.Value.Type == SynchronizationType.Get)
                            {
                                // update or insert we can decide on device
                                cmd.CommandText = string.Format("SELECT COUNT(ID) FROM DiaryItem WHERE ID = '{0}'", download.Key);
                                if ((int)cmd.ExecuteScalar() > 0)
                                {
                                    cmd.CommandText = string.Format(
                                        "UPDATE DiaryItem SET Content = '{1}', UpdatedVersion = {2}, UpdatedDeviceID = '{3}', BasedVersion = {2}, BasedDeviceID = '{3}', IsDeleted = '{4}', UpdatedOn = '{5}' WHERE ID = '{0}'",
                                        download.Key,
                                        download.Value.Content,
                                        download.Value.UpdatedVersion,
                                        download.Value.UpdatedDeviceID,
                                        download.Value.IsDeleted,
                                        download.Value.UpdatedOn);
                                }
                                else
                                {
                                    cmd.CommandText = string.Format(
                                        "INSERT INTO DiaryItem VALUES ('{0}', '{1}', {2}, '{3}', {2}, '{3}', '{4}', '{5}')",
                                        download.Key,
                                        download.Value.Content,
                                        download.Value.UpdatedVersion,
                                        download.Value.UpdatedDeviceID,
                                        download.Value.IsDeleted,
                                        download.Value.UpdatedOn);
                                }
                                cmd.ExecuteNonQuery();
                            }
                            else
                            {
                                throw new ApplicationException(string.Format("Invalid type ({0}) on item ({1}).", download.Value.Type, download.Key));
                            }
                        }
                        // update the local based information on those changed by this device to the updated information
                        cmd.CommandText = string.Format("UPDATE DiaryItem SET BasedVersion = UpdatedVersion, BasedDeviceID = UpdatedDeviceID WHERE UpdatedDeviceID = '{0}'", _deviceId);
                        cmd.ExecuteNonQuery();
                        // update the max updated version if needed
                        var expectUpdatedVersion = synchronizeResponse.MaxUpdateVersion + 1;
                        cmd.CommandText = "SELECT Value FROM System WHERE Name = 'updated.version'";
                        var actualUpdatedVersion = long.Parse((string)cmd.ExecuteScalar());
                        if (actualUpdatedVersion != expectUpdatedVersion)
                        {
                            cmd.CommandText = string.Format("UPDATE System SET Value = '{0}' WHERE Name = 'updated.version'", expectUpdatedVersion);
                            cmd.ExecuteNonQuery();
                        }
                        // update the last sync on
                        cmd.CommandText = string.Format("UPDATE System SET Value = '{0}' WHERE Name = 'last.sync.on'", ToGlobalDateTimeString(thisSyncOn));
                        cmd.ExecuteNonQuery();
                        tran.Commit();
                    }
                    catch
                    {
                        if (tran != null)
                        {
                            tran.Rollback();
                        }
                        throw;
                    }
                }
            }

            // end of the sync
            Console.WriteLine("[SYN] Done.");
            Console.WriteLine("[SYN] =====");
        }

        private static void ShowTips()
        {
            Console.WriteLine("[INF] a - Append a new diary item.");
            Console.WriteLine("[INF] e - Edit an existing diary item.");
            Console.WriteLine("[INF] d - Delete an existing diary item.");
            Console.WriteLine("[INF] l - List all items in local storage.");
            Console.WriteLine("[INF] s - Sync to the server.");
            Console.WriteLine("[INF] q - Exit the emulator.");
            Console.WriteLine("[INF] h - Show this tips.");
        }

        private static string Prompt()
        {
            Console.Write("$$$>");
            return Console.ReadLine();
        }

        private static long PopUpdateVersion()
        {
            long version;
            using (var conn = Helper.CreateConnection(_database))
            {
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = "SELECT Value FROM System WHERE Name = 'updated.version'";
                    version = long.Parse((string)cmd.ExecuteScalar());
                    cmd.CommandText = "UPDATE System SET Value = Value + 1 WHERE Name = 'updated.version'";
                    cmd.ExecuteNonQuery();
                }
            }
            return version;
        }

        private static string GetLastSyncOn()
        {
            string lastSyncOn = string.Empty;
            using (var conn = Helper.CreateConnection(_database))
            {
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = "SELECT Value FROM System WHERE Name = 'last.sync.on'";
                    lastSyncOn = cmd.ExecuteScalar().ToString();
                }
            }
            if (string.IsNullOrWhiteSpace(lastSyncOn))
            {
                lastSyncOn = ToGlobalDateTimeString(new DateTime(1753,1,1,0,0,0));
            }
            return lastSyncOn;
        }

        private static string ToGlobalDateTimeString(DateTime dt)
        {
            return dt.ToString("yyyy-MM-dd HH:mm:ss.fff");
        }
    }
}
